<template>
    <div class="el-tree"
         :class="treeClass"
         role="tree">
        <el-tree-node
            v-for="child in root.childNodes"
            :node="child"
            :props="props"
            :render-after-expand="renderAfterExpand"
            :show-checkbox="showCheckbox"
            :key="getNodeKey(child)"
            :render-content="renderContent"
            @node-expand="handleNodeExpand">
        </el-tree-node>
        <div class="el-tree__empty-block" v-if="isEmpty">
            <span class="el-tree__empty-text">{{ emptyText }}</span>
        </div>
        <div
            v-show="dragState.showDropIndicator"
            class="el-tree__drop-indicator"
            ref="dropIndicator">
        </div>
    </div>
</template>

<script>
import TreeStore from './model/tree-store';
import { findNearestComponent, getNodeKey } from './model/util';
import ElTreeNode from './tree-node.vue';
import { t } from 'element-ui/src/locale';
import emitter from 'element-ui/src/mixins/emitter';
import { addClass, removeClass } from 'element-ui/src/utils/dom';

export default {
    name: 'ElTree',
    mixins: [ emitter ],
    components: {
        ElTreeNode
    },
    data() {
        return {
            store: null,
            root: null,
            currentNode: null,
            treeItems: null,
            checkboxItems: [],
            dragState: {
                showDropIndicator: false,
                draggingNode: null,
                dropNode: null,
                allowDrop: true
            }
        };
    },
    props: {
        data: {
            type: Array
        },
        emptyText: {
            type: String,
            default() {
                return t('el.tree.emptyText');
            }
        },
        renderAfterExpand: {
            type: Boolean,
            default: true
        },
        nodeKey: String,
        checkStrictly: Boolean,
        defaultExpandAll: Boolean,
        expandOnClickNode: {
            type: Boolean,
            default: true
        },
        checkOnClickNode: Boolean,
        checkDescendants: {
            type: Boolean,
            default: false
        },
        autoExpandParent: {
            type: Boolean,
            default: true
        },
        defaultCheckedKeys: Array,
        defaultExpandedKeys: Array,
        currentNodeKey: [ String, Number ],
        renderContent: Function,
        showCheckbox: {
            type: Boolean,
            default: false
        },
        draggable: {
            type: Boolean,
            default: false
        },
        allowDrag: Function,
        allowDrop: Function,
        props: {
            default() {
                return {
                    children: 'children',
                    label: 'label',
                    disabled: 'disabled'
                };
            }
        },
        lazy: {
            type: Boolean,
            default: false
        },
        highlightCurrent: Boolean,
        load: Function,
        filterNodeMethod: Function,
        accordion: Boolean,
        indent: {
            type: Number,
            default: 18
        },
        iconClass: String
    },
    computed: {
        children: {
            set(value) {
                this.data = value;
            },
            get() {
                return this.data;
            }
        },
        treeItemArray() {
            return Array.prototype.slice.call(this.treeItems);
        },
        isEmpty() {
            const { childNodes } = this.root;
            return !childNodes || childNodes.length === 0 || childNodes.every(({ visible }) => !visible);
        },
        treeClass() {
            return {
                'el-tree--highlight-current': this.highlightCurrent,
                'is-dragging': !!this.dragState.draggingNode,
                'is-drop-not-allow': !this.dragState.allowDrop,
                'is-drop-inner': this.dragState.dropType === 'inner',
            };
        },
    },
    watch: {
        defaultCheckedKeys(newVal) {
            this.store.setDefaultCheckedKey(newVal);
        },
        defaultExpandedKeys(newVal) {
            this.store.defaultExpandedKeys = newVal;
            this.store.setDefaultExpandedKeys(newVal);
        },
        data(newVal) {
            this.store.setData(newVal);
        },
        checkboxItems(val) {
            Array.prototype.forEach.call(val, (checkbox) => {
                checkbox.setAttribute('tabindex', -1);
            });
        },
        checkStrictly(newVal) {
            this.store.checkStrictly = newVal;
        }
    },
    methods: {
        filter(value) {
            if (!this.filterNodeMethod) throw new Error('[Tree] filterNodeMethod is required when filter');
            this.store.filter(value);
        },
        getNodeKey(node) {
            return getNodeKey(this.nodeKey, node.data);
        },
        getNodePath(data) {
            if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in getNodePath');
            const node = this.store.getNode(data);
            if (!node) return [];
            const path = [ node.data ];
            let parent = node.parent;
            while (parent && parent !== this.root) {
                path.push(parent.data);
                parent = parent.parent;
            }
            return path.reverse();
        },
        getCheckedNodes(leafOnly, includeHalfChecked) {
            return this.store.getCheckedNodes(leafOnly, includeHalfChecked);
        },
        getCheckedKeys(leafOnly) {
            return this.store.getCheckedKeys(leafOnly);
        },
        getCurrentNode() {
            const currentNode = this.store.getCurrentNode();
            return currentNode ? currentNode.data : null;
        },
        getCurrentKey() {
            if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in getCurrentKey');
            const currentNode = this.getCurrentNode();
            return currentNode ? currentNode[this.nodeKey] : null;
        },
        setCheckedNodes(nodes, leafOnly) {
            if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedNodes');
            this.store.setCheckedNodes(nodes, leafOnly);
        },
        setCheckedKeys(keys, leafOnly) {
            if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCheckedKeys');
            this.store.setCheckedKeys(keys, leafOnly);
        },
        setChecked(data, checked, deep) {
            this.store.setChecked(data, checked, deep);
        },
        getHalfCheckedNodes() {
            return this.store.getHalfCheckedNodes();
        },
        getHalfCheckedKeys() {
            return this.store.getHalfCheckedKeys();
        },
        setCurrentNode(node) {
            if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentNode');
            this.store.setUserCurrentNode(node);
        },
        setCurrentKey(key) {
            if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in setCurrentKey');
            this.store.setCurrentNodeKey(key);
        },
        getNode(data) {
            return this.store.getNode(data);
        },
        remove(data) {
            this.store.remove(data);
        },
        append(data, parentNode) {
            this.store.append(data, parentNode);
        },
        insertBefore(data, refNode) {
            this.store.insertBefore(data, refNode);
        },
        insertAfter(data, refNode) {
            this.store.insertAfter(data, refNode);
        },
        handleNodeExpand(nodeData, node, instance) {
            this.broadcast('ElTreeNode', 'tree-node-expand', node);
            this.$emit('node-expand', nodeData, node, instance);
        },
        updateKeyChildren(key, data) {
            if (!this.nodeKey) throw new Error('[Tree] nodeKey is required in updateKeyChild');
            this.store.updateChildren(key, data);
        },
        initTabIndex() {
            this.treeItems = this.$el.querySelectorAll('.is-focusable[role=treeitem]');
            this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]');
            const checkedItem = this.$el.querySelectorAll('.is-checked[role=treeitem]');
            if (checkedItem.length) {
                checkedItem[0].setAttribute('tabindex', 0);
                return;
            }
            this.treeItems[0] && this.treeItems[0].setAttribute('tabindex', 0);
        },
        handleKeydown(ev) {
            const currentItem = ev.target;
            if (currentItem.className.indexOf('el-tree-node') === -1) return;
            const keyCode = ev.keyCode;
            this.treeItems = this.$el.querySelectorAll('.is-focusable[role=treeitem]');
            const currentIndex = this.treeItemArray.indexOf(currentItem);
            let nextIndex;
            if ([ 38, 40 ].indexOf(keyCode) > -1) { // up、down
                ev.preventDefault();
                if (keyCode === 38) { // up
                    nextIndex = currentIndex !== 0 ? currentIndex - 1 : 0;
                } else {
                    nextIndex = (currentIndex < this.treeItemArray.length - 1) ? currentIndex + 1 : 0;
                }
                this.treeItemArray[nextIndex].focus(); // 选中
            }
            if ([ 37, 39 ].indexOf(keyCode) > -1) { // left、right 展开
                ev.preventDefault();
                currentItem.click(); // 选中
            }
            const hasInput = currentItem.querySelector('[type="checkbox"]');
            if ([ 13, 32 ].indexOf(keyCode) > -1 && hasInput) { // space enter选中checkbox
                ev.preventDefault();
                hasInput.click();
            }
        }
    },
    created() {
        this.isTree = true;
        this.store = new TreeStore({
            key: this.nodeKey,
            data: this.data,
            lazy: this.lazy,
            props: this.props,
            load: this.load,
            currentNodeKey: this.currentNodeKey,
            checkStrictly: this.checkStrictly,
            checkDescendants: this.checkDescendants,
            defaultCheckedKeys: this.defaultCheckedKeys,
            defaultExpandedKeys: this.defaultExpandedKeys,
            autoExpandParent: this.autoExpandParent,
            defaultExpandAll: this.defaultExpandAll,
            filterNodeMethod: this.filterNodeMethod
        });
        this.root = this.store.root;
        let dragState = this.dragState;
        this.$on('tree-node-drag-start', (event, treeNode) => {
            if (typeof this.allowDrag === 'function' && !this.allowDrag(treeNode.node)) {
                event.preventDefault();
                return false;
            }
            event.dataTransfer.effectAllowed = 'move';
            // wrap in try catch to address IE's error when first param is 'text/plain'
            try {
                // setData is required for draggable to work in FireFox
                // the content has to be '' so dragging a node out of the tree won't open a new tab in FireFox
                event.dataTransfer.setData('text/plain', '');
            } catch (e) {
            }
            dragState.draggingNode = treeNode;
            this.$emit('node-drag-start', treeNode.node, event);
        });
        this.$on('tree-node-drag-over', (event, treeNode) => {
            const dropNode = findNearestComponent(event.target, 'ElTreeNode');
            const oldDropNode = dragState.dropNode;
            if (oldDropNode && oldDropNode !== dropNode) {
                removeClass(oldDropNode.$el, 'is-drop-inner');
            }
            const draggingNode = dragState.draggingNode;
            if (!draggingNode || !dropNode) return;
            let dropPrev = true;
            let dropInner = true;
            let dropNext = true;
            let userAllowDropInner = true;
            if (typeof this.allowDrop === 'function') {
                dropPrev = this.allowDrop(draggingNode.node, dropNode.node, 'prev');
                userAllowDropInner = dropInner = this.allowDrop(draggingNode.node, dropNode.node, 'inner');
                dropNext = this.allowDrop(draggingNode.node, dropNode.node, 'next');
            }
            event.dataTransfer.dropEffect = dropInner ? 'move' : 'none';
            if ((dropPrev || dropInner || dropNext) && oldDropNode !== dropNode) {
                if (oldDropNode) {
                    this.$emit('node-drag-leave', draggingNode.node, oldDropNode.node, event);
                }
                this.$emit('node-drag-enter', draggingNode.node, dropNode.node, event);
            }
            if (dropPrev || dropInner || dropNext) {
                dragState.dropNode = dropNode;
            }
            if (dropNode.node.nextSibling === draggingNode.node) {
                dropNext = false;
            }
            if (dropNode.node.previousSibling === draggingNode.node) {
                dropPrev = false;
            }
            if (dropNode.node.contains(draggingNode.node, false)) {
                dropInner = false;
            }
            if (draggingNode.node === dropNode.node || draggingNode.node.contains(dropNode.node)) {
                dropPrev = false;
                dropInner = false;
                dropNext = false;
            }
            const targetPosition = dropNode.$el.getBoundingClientRect();
            const treePosition = this.$el.getBoundingClientRect();
            let dropType;
            const prevPercent = dropPrev ? (dropInner ? 0.25 : (dropNext ? 0.45 : 1)) : -1;
            const nextPercent = dropNext ? (dropInner ? 0.75 : (dropPrev ? 0.55 : 0)) : 1;
            let indicatorTop = -9999;
            const distance = event.clientY - targetPosition.top;
            if (distance < targetPosition.height * prevPercent) {
                dropType = 'before';
            } else if (distance > targetPosition.height * nextPercent) {
                dropType = 'after';
            } else if (dropInner) {
                dropType = 'inner';
            } else {
                dropType = 'none';
            }
            const iconPosition = dropNode.$el.querySelector('.el-tree-node__expand-icon').getBoundingClientRect();
            const dropIndicator = this.$refs.dropIndicator;
            if (dropType === 'before') {
                indicatorTop = iconPosition.top - treePosition.top;
            } else if (dropType === 'after') {
                indicatorTop = iconPosition.bottom - treePosition.top;
            }
            dropIndicator.style.top = indicatorTop + 'px';
            dropIndicator.style.left = (iconPosition.right - treePosition.left) + 'px';
            if (dropType === 'inner') {
                addClass(dropNode.$el, 'is-drop-inner');
            } else {
                removeClass(dropNode.$el, 'is-drop-inner');
            }
            dragState.showDropIndicator = dropType === 'before' || dropType === 'after';
            dragState.allowDrop = dragState.showDropIndicator || userAllowDropInner;
            dragState.dropType = dropType;
            this.$emit('node-drag-over', draggingNode.node, dropNode.node, event);
        });
        this.$on('tree-node-drag-end', (event) => {
            const { draggingNode, dropType, dropNode } = dragState;
            event.preventDefault();
            event.dataTransfer.dropEffect = 'move';
            if (draggingNode && dropNode) {
                const draggingNodeCopy = { data: draggingNode.node.data };
                if (dropType !== 'none') {
                    draggingNode.node.remove();
                }
                if (dropType === 'before') {
                    dropNode.node.parent.insertBefore(draggingNodeCopy, dropNode.node);
                } else if (dropType === 'after') {
                    dropNode.node.parent.insertAfter(draggingNodeCopy, dropNode.node);
                } else if (dropType === 'inner') {
                    dropNode.node.insertChild(draggingNodeCopy);
                }
                if (dropType !== 'none') {
                    this.store.registerNode(draggingNodeCopy);
                }
                removeClass(dropNode.$el, 'is-drop-inner');
                this.$emit('node-drag-end', draggingNode.node, dropNode.node, dropType, event);
                if (dropType !== 'none') {
                    this.$emit('node-drop', draggingNode.node, dropNode.node, dropType, event);
                }
            }
            if (draggingNode && !dropNode) {
                this.$emit('node-drag-end', draggingNode.node, null, dropType, event);
            }
            dragState.showDropIndicator = false;
            dragState.draggingNode = null;
            dragState.dropNode = null;
            dragState.allowDrop = true;
        });
    },
    mounted() {
        this.initTabIndex();
        this.$el.addEventListener('keydown', this.handleKeydown);
    },
    updated() {
        this.treeItems = this.$el.querySelectorAll('[role=treeitem]');
        this.checkboxItems = this.$el.querySelectorAll('input[type=checkbox]');
    }
};
</script>
